前言
早在多年前,lazyload 已经出现了,懒加载在前端里边同样具有十分重要的意义。react-lazyload 的作用是当组件未出现在屏幕内时,不去挂载该组件,而是使用 placeholder 去渲染,让滚动使内容出现后,组件会被挂载。就是这么简单!例如,一个复杂的组件(非首屏内容),使用了懒加载后,渲染首屏就会节省很多资源,从而减少首屏渲染时间。
Demo
源码地址 react-lazyload
Demo地址 Demo
HelloWorld
将需要懒加载的组件使用 LazyLoad 包裹即可,最好使用 height 进行站位,否则该组件位置将会为 0
1 2 3 4 5 6 7 <LazyLoad height={200}> <img src="tiger.jpg" /> /* Lazy loading images is supported out of box, no extra config needed, set `height` for better experience */ </LazyLoad>
解析
从源码角度分析~
一览核心
本小节摘取了最核心的代码,目的在于对 LazyLoad 组件有个最核心的认识,它的核心就是监听滚动事件,检查组件是否在屏幕内,如果在的话就显示,不在的话就不显示~
1 2 3 4 5 6 7 8 9 10 11 12 13 class LazyLoad extends Component { componentDidMount() { on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); } render() { return this.visible ? this.props.children : this.props.placeholder ? this.props.placeholder : <div style={{ height: this.props.height }} className="lazyload-placeholder" ref={this.setRef} />; } }
LazyLoad 的属性,透过属性,我们可以知道它大概有些什么功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 LazyLoad.propTypes = { once: PropTypes.bool, height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), offset: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]), overflow: PropTypes.bool, // 不是 window 滚动,而使用了 overflow: scroll resize: PropTypes.bool, // 是否监听 resize scroll: PropTypes.bool, // 是否监听滚动 children: PropTypes.node, throttle: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), debounce: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), placeholder: PropTypes.node, scrollContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), unmountIfInvisible: PropTypes.bool, preventLoading: PropTypes.bool }; // 默认值 LazyLoad.defaultProps = { once: false, offset: 0, overflow: false, resize: false, scroll: true, unmountIfInvisible: false, preventLoading: false, };
完整的 componentDidMount,scrollport 是滚动试图,默认是 window,如果 props 传入了 scrollContainer,那么滚动试图将是自定义的。needResetFinalLazyLoadHandler 是控制是否重置滚动监听。debounce 和 throttle 分别是用来控制滚动事件的监听触发频率,默认都是 undefine,needResetFinalLazyLoadHandler 初始值为 false。finalLazyLoadHandler 初始值也为 undefine,而 overflow 也为 false,scroll 为 true,listeners 是需要懒加载的组件集合,初始大小肯定为0,componentDidMount 最后才会进行添加,因此最终会走到 **on(scrollport, ‘scroll’, finalLazyLoadHandler, passiveEvent),事件只需要一次绑定即可。
**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 componentDidMount() { // It's unlikely to change delay type on the fly, this is mainly // designed for tests let scrollport = window; const { scrollContainer, } = this.props; if (scrollContainer) { if (isString(scrollContainer)) { scrollport = scrollport.document.querySelector(scrollContainer); } } const needResetFinalLazyLoadHandler = (this.props.debounce !== undefined && delayType === 'throttle') || (delayType === 'debounce' && this.props.debounce === undefined); if (needResetFinalLazyLoadHandler) { off(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); off(window, 'resize', finalLazyLoadHandler, passiveEvent); finalLazyLoadHandler = null; } if (!finalLazyLoadHandler) { if (this.props.debounce !== undefined) { finalLazyLoadHandler = debounce(lazyLoadHandler, typeof this.props.debounce === 'number' ? this.props.debounce : 300); delayType = 'debounce'; } else if (this.props.throttle !== undefined) { finalLazyLoadHandler = throttle(lazyLoadHandler, typeof this.props.throttle === 'number' ? this.props.throttle : 300); delayType = 'throttle'; } else { finalLazyLoadHandler = lazyLoadHandler; } } if (this.props.overflow) { const parent = scrollParent(this.ref); if (parent && typeof parent.getAttribute === 'function') { const listenerCount = 1 + (+parent.getAttribute(LISTEN_FLAG)); if (listenerCount === 1) { parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent); } parent.setAttribute(LISTEN_FLAG, listenerCount); } } else if (listeners.length === 0 || needResetFinalLazyLoadHandler) { const { scroll, resize } = this.props; if (scroll) { on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); } if (resize) { on(window, 'resize', finalLazyLoadHandler, passiveEvent); } } listeners.push(this); checkVisible(this); }
通常 finalLazyLoadHandler 就是 lazyLoadHandler,不会对滚动事件进行 debounce 或 throttle,我们一般为了性能,会使用 throttle 进行处理。函数会对每一个懒加载组件进行 checkVisible,之后会移除 once component
1 2 3 4 5 6 7 8 const lazyLoadHandler = () => { for (let i = 0; i < listeners.length; ++i) { const listener = listeners[i]; checkVisible(listener); } // Remove `once` component in listeners purgePending(); };
checkVisible,检查组件是否出现在 viewport 中,如果出现了就吧 visible 设置为 true,当然如果设置了 unmountIfInvisible = true,那么不可见时组件将被移除,如果之前已经渲染了,需要避免再次渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const checkVisible = function checkVisible(component) { const node = component.ref; if (!(node instanceof HTMLElement)) { return; } const parent = scrollParent(node); const isOverflow = component.props.overflow && parent !== node.ownerDocument && parent !== document && parent !== document.documentElement; const visible = isOverflow ? checkOverflowVisible(component, parent) : checkNormalVisible(component); if (visible) { // Avoid extra render if previously is visible if (!component.visible && !component.preventLoading) { if (component.props.once) { pending.push(component); } component.visible = true; component.forceUpdate(); } } else if (!(component.props.once && component.visible)) { component.visible = false; if (component.props.unmountIfInvisible) { component.forceUpdate(); } } };
checkNormalVisible 检查组件是否 visible 的函数,判断组件的getgetBoundingClientRect 的 top - offset(相对于屏幕顶部的距离) 与 window 的 height 之间的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const checkNormalVisible = function checkNormalVisible(component) { const node = component.ref; // If this element is hidden by css rules somehow, it's definitely invisible if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false; let top; let elementHeight; try { // 这个语法 node 也是支持的 ({ top, height: elementHeight } = node.getBoundingClientRect()); } catch (e) { ({ top, height: elementHeight } = defaultBoundingClientRect); } const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight; const offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset]; // Be compatible with previous API return (top - offsets[0] <= windowInnerHeight) && (top + elementHeight + offsets[1] >= 0); };
1 2 (top - offsets[0] <= windowInnerHeight) && (top + elementHeight + offsets[1] >= 0);
一张图解析!
到这里解析的差不多了
欣赏一下 throttle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default function throttle(fn, threshhold, scope) { threshhold || (threshhold = 250); var last, deferTimer; return function () { var context = scope || this; var now = +new Date, args = arguments; if (last && now < last + threshhold) { // hold on to it clearTimeout(deferTimer); deferTimer = setTimeout(function () { last = now; fn.apply(context, args); }, threshhold); } else { last = now; fn.apply(context, args); } }; }
再欣赏一下 debounce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 export default function debounce(func, wait, immediate) { let timeout; let args; let context; let timestamp; let result; const later = function later() { const last = +(new Date()) - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) { context = null; args = null; } } } }; return function debounced() { context = this; args = arguments; timestamp = +(new Date()); const callNow = immediate && !timeout; if (!timeout) { timeout = setTimeout(later, wait); } if (callNow) { result = func.apply(context, args); context = null; args = null; } return result; }; }
获取 scrollParent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 export default (node) => { if (!(node instanceof HTMLElement)) { return document.documentElement; } const excludeStaticParent = node.style.position === 'absolute'; const overflowRegex = /(scroll|auto)/; let parent = node; while (parent) { if (!parent.parentNode) { return node.ownerDocument || document.documentElement; } const style = window.getComputedStyle(parent); const position = style.position; const overflow = style.overflow; const overflowX = style['overflow-x']; const overflowY = style['overflow-y']; if (position === 'static' && excludeStaticParent) { parent = parent.parentNode; continue; } if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) { return parent; } parent = parent.parentNode; } return node.ownerDocument || node.documentElement || document.documentElement; };
总结思考
我们可以看到,Lazyload 并不能实现类似客户端的图片懒加载,Lazyload 加载图片也会出现白屏时间,解决办法是使用 image.onload ,当图片资源请求关闭后,再显示图片,就可以做到类似客户端的效果。